<template>
  <div ref="recordRef" class="record">
    <ul ref="messageUlRef" v-if="data.size">
      <li class="clearfix" v-for="([key, messageData], index) in data" :key="key">
        <div class="record-loadMore" v-if="!isLast && index === 0">
          <span @click="loadingNextPage(key)">
            <SvgIcon
              v-if="isLoadingNePage"
              icon-class="loadingCircle"
              popper-class="loadingCircle"
            />
            加载更多</span
          >
        </div>
        <p class="record-time" v-if="index === 0">
          <span v-time.show="messageData.time"></span>
        </p>
        <p
          class="record-time"
          v-else-if="([...data][index] as any)[1].time - ([...data][index - 1] as any)[1].time >= 600000"
        >
          <span v-time.show="messageData.time"></span>
        </p>
        <div
          :class="{
            'chatter-other': user.userId !== messageData.userId,
            'chatter-me': user.userId === messageData.userId,
            clearfix: true,
          }"
        >
          <div
            :class="{
              flex: true,
              'chatter-check': true,
              'chatter-check__other': user.userId !== messageData.userId,
              'chatter-check__me': user.userId === messageData.userId,
              isChecked: selectMessageIds.has(key),
              noChecked: !selectMessageIds.has(key),
            }"
            v-if="showSelect && !messageData.isLoading"
            @click="selectMess(key)"
          >
            <SvgIcon popper-class="icon-checked" icon-class="check" />
          </div>
          <div
            :class="{
              clearfix: true,
              'chatter-other': user.userId !== messageData.userId,
              'chatter-me': user.userId === messageData.userId,
            }"
          >
            <div
              class="chatter-disabled"
              v-if="showSelect"
              @click="selectMess(key)"
            ></div>
            <img
              :class="{
                'chatter-avatar': true,
                'chatter-other': user.userId !== messageData.userId,
                'chatter-me': user.userId === messageData.userId,
              }"
              :src="messageData.avatar"
              alt="无法显示此图片"
              @click="openInfo(messageData.userId, $event)"
            />
            <div
              :class="{
                clearfix: true,
                'chatter-info': true,
                'chatter-info__last': index === data!.size - 1,
              }"
            >
              <SvgIcon
                v-if="messageData.isError"
                icon-class="errorexp"
                :class="{
                  'loadingCircle-opearMess__other': user.userId !== messageData.userId,
                  'loadingCircle-opearMess__me': user.userId === messageData.userId,
                }"
                style="cursor: pointer"
                @click="
                  retryMess({
                    id: key,
                    content: messageData.message,
                    type: messageData.type,
                    duration: messageData.duration,
                  })
                "
              />
              <SvgIcon
                v-else-if="messageData.isLoading"
                icon-class="loadingCircle"
                :class="{
                  'loadingCircle-opearMess': true,
                  'loadingCircle-opearMess__other': user.userId !== messageData.userId,
                  'loadingCircle-opearMess__me': user.userId === messageData.userId,
                }"
              />
              <div
                :class="{
                  'chatter-info__content': true,
                  'chatter-info__content--other': user.userId !== messageData.userId,
                  'chatter-info__content--me': user.userId === messageData.userId,
                }"
                @contextmenu.prevent="openMenu(key, messageData, $event)"
              >
                <p
                  v-if="messageData.type === 'text'"
                  v-html="replaceEmotion(messageData.message, 'chat-message__emotion')"
                ></p>
                <play-audio
                  v-else-if="messageData.type === 'voice'"
                  :src="messageData.message"
                  :duration="messageData.duration"
                  :wave-surfer="WaveSurfer"
                  :is-self="user.userId === messageData.userId"
                ></play-audio>
              </div>
              <template v-if="messageData?.time"
                ><p class="chatter-info__time" v-time.show="messageData.time"></p
              ></template>
            </div>
          </div>
        </div>
      </li>
    </ul>
  </div>
  <img-browse
    v-model="showImgBrowse"
    v-model:img-index="imgIndex"
    :imgs="imgs"
  ></img-browse>
  <template v-if="show"
    ><operate-mess
      ref="messOpeMenuRef"
      v-model="showMenu"
      :other-id="otherId"
      v-model:show-select="showSelect"
      :select-ids="selectMessageIds"
      :message-id="messageId"
      :message-data="messageData!"
    ></operate-mess>
    <usr-info-card
      ref="userInfoRef"
      v-model="showInfo"
      :user-id="currentId"
      @update-position="updatePosition"
    ></usr-info-card
  ></template>
</template>

<script lang="ts" setup>
import { ref, reactive, nextTick, watch, onMounted, onBeforeUnmount } from "vue";
import { useUserPinia } from "@/pinia/user";
import { useMessagePinia } from "@/pinia/messages";
import { storeToRefs } from "pinia";
import { useSocketPinia } from "@/pinia/socket";
import vTime from "@/directives/time";
import { ElMessage } from "element-plus";
import type Stomp from "stompjs";
import Cookie from "@/plugins/cookie";
import operateMess from "./operateMess.vue";
import usrInfoCard from "./usrInfoCard.vue";
import imgBrowse from "@/components/ui-components/imgBrowse.vue";
import Trigger from "@/plugins/rigClickOpMen";
import playAudio from "./audio.vue";
import WaveSurfer from "wavesurfer";
import { replaceEmotion } from "@/directives/filterText";

const props = withDefaults(
  defineProps<{
    // 具体的聊天数据
    data: Map<string, Message>;
    isLast?: boolean;
    // 对方的userId
    otherId: string;
    /**
     * 滚动条当前已经滚动的距离
     */
    scrollTop: number;
  }>(),
  {
    isLast: true,
  }
);
const emits = defineEmits<{
  // 滚动到底部
  (e: "scrollToTop", top: number): void;
  // 更新滚动条的状态
  (e: "updateScroll"): void;
  // 重新发送消息(文本)
  (e: "retrySendMessageTxt", id: string, conent: string): void;
  // 重发语音消息
  (
    e: "retrySendMessageRecord",
    payload: {
      content: string;
      duration: number;
      id: string;
    }
  ): void;
}>();

const messageStore = useMessagePinia();
const { user } = storeToRefs(useUserPinia());
const socketStore = useSocketPinia();

/**
 * 给该组件的根元素一个ref属性，便于通过$refs访问
 */
const recordRef = ref<HTMLDivElement>(document.createElement("div"));
// 请求下一页数据是否结束
const isLoadingNePage = ref<boolean>(false);
/**
 * 当前聊天数据的第一条信息的id值
 */
const firstMessId = ref<string>("");
/**
 * 点击加载更多前时该组件最多可滚动的距离
 */
const beforeScrollMax = ref<number>(0);
// 用户请求下一页聊天数据
const loadingNextPage = (messageId: string) => {
  return new Promise<number>((resove, reject) => {
    if (!isLoadingNePage.value) {
      isLoadingNePage.value = true;
      beforeScrollMax.value = recordRef.value.scrollHeight;
      nextTick(() => {
        messageStore
          .getPartMessages({
            userId: props.otherId,
            messageId,
          })
          .then((code: number) => {
            isLoadingNePage.value = false;
            if (code === 1)
              setTimeout(() => {
                emits(
                  "scrollToTop",
                  recordRef.value.scrollHeight - beforeScrollMax.value
                );
                firstMessId.value = [...props.data][0][0];
              });
            else if (code === -2)
              ElMessage({
                type: "error",
                duration: 1500,
                message: "加载失败，对方和自己不是好友关系",
              });
            else
              ElMessage({
                type: "error",
                duration: 1500,
                message: "加载失败",
              });
            resove(code);
          })
          .catch(() => {
            isLoadingNePage.value = false;
            ElMessage({
              type: "error",
              duration: 1500,
              message: "加载失败",
            });
            reject();
          });
      });
    }
  });
};

// 是否显示图片浏览组件
let showImgBrowse = ref<boolean>(false);

/**
 * 当前聊天记录中的所有图片集合
 */
const imgs = reactive<string[]>([]);
// 当前用户所浏览的图片的索引值
const imgIndex = ref<number>(0);
const messageUlRef = ref<HTMLDivElement>();

/**
 * 给该聊天记录的所有图片添加点击查看事件
 */
const addAllImgClick = () => {
  const imgsEle = Array.from(
    messageUlRef.value!.getElementsByClassName("chat-message__img")
  ) as HTMLImageElement[];
  imgs.length && imgs.splice(0);
  imgsEle.forEach((item, index) => {
    imgs.push(item.src);
    item.onclick = () => {
      imgIndex.value = index;
      showImgBrowse.value = true;
    };
  });
};

watch(
  () => messageStore.isSendImg,
  (newValue: boolean) =>
    setTimeout(() => {
      if (newValue) {
        addAllImgClick();
        messageStore.isSendImg = false;
      }
    }, 100)
);

/**
 * 滚动到底部
 */
const scrollBottom = () => {
  const top: number = recordRef.value.scrollHeight - recordRef.value.offsetHeight;
  top > 0 && emits("scrollToTop", top);
};

watch(
  () => props.otherId,
  () => {
    nextTick(() => {
      if (messageUlRef.value) {
        const imgsEle = Array.from(
          messageUlRef.value.getElementsByClassName("chat-message__img")
        ) as HTMLImageElement[];
        imgs.length && imgs.splice(0);
        imgsEle.forEach((item) => imgs.push(item.src));
        addAllImgClick();
        scrollBottom();
        emits("updateScroll");
      }
      props.data.size ? (firstMessId.value = [...props.data][0][0]) : undefined;
    });
  },
  {
    immediate: true,
  }
);

watch(
  () => props.data.size,
  (newSize, oldSize) => {
    if (!newSize) {
      !props.isLast &&
        loadingNextPage(firstMessId.value).then((code) => {
          if (code === 1)
            nextTick(() => {
              addAllImgClick();
              setTimeout(() => scrollBottom());
            });
        });
    } else {
      nextTick(() => {
        addAllImgClick();
        setTimeout(() => {
          emits("updateScroll");
          // 当对方发来消息时此时滚动条剩余滚动距离小于等于670时则滚动到底部
          if (newSize > oldSize) {
            const overScrollLen: number =
              recordRef.value.scrollHeight -
              props.scrollTop -
              recordRef.value.offsetHeight;
            overScrollLen <= 670 && scrollBottom();
          }
        });
      });
    }
  }
);

/**
 * 是否显示勾选信息框
 */
const showSelect = ref<boolean>(false);
/**
 * 当前用户所勾选的消息的messageId
 */
const selectMessageIds = reactive<Set<string>>(new Set());

// 勾选或者取消勾选某条消息
const selectMess = (key: string) => {
  if (!selectMessageIds.has(key)) selectMessageIds.add(key);
  else selectMessageIds.delete(key);
};

const triggerOper = ref<Trigger>();
const triggerUserInfo = ref<Trigger>();
/**
 * 是否需要打开鼠标右击后出现的菜单
 */
const showMenu = ref<boolean>(false);
/**
 * 给鼠标右击出现的菜单元素组件一个ref属性，便于通过$refs访问
 */
const messOpeMenuRef = ref<typeof operateMess>();
/**
 * 当前用户选择的聊天会话对象的userId
 */
const messageId = ref<string>("");
/**
 * 当前用户操作的消息数据
 */
const messageData = ref<Message>();

const closeMenu = () => {
  if (showMenu.value) showMenu.value = false;
};

watch(
  () => showMenu.value,
  (newValue: boolean) => {
    if (newValue) triggerOper.value?.resize();
    else triggerOper.value?.removeResize();
  }
);

/**
 * 鼠标右键打开操作菜单
 */
const openMenu: (key: string, data: Message, $event: MouseEvent) => void = (
  key: string,
  data: Message,
  $event: MouseEvent
): void => {
  if (!data.isLoading) {
    if (!showMenu.value) showMenu.value = true;
    messageId.value = key;
    messageData.value = data;
    nextTick(() =>
      triggerOper.value?.computedDirection(
        messOpeMenuRef.value?.$refs.messOpeMenuRef as HTMLDListElement,
        $event
      )
    );
  }
};

/**
 * 是否需要打开用户信息卡片
 */
const showInfo = ref<boolean>(false);
/**
 * 给资料卡片组件一个ref属性，便于通过$refs访问
 */
const userInfoRef = ref<typeof usrInfoCard>();

watch(
  () => showInfo.value,
  (newValue: boolean) => {
    if (newValue) triggerUserInfo.value?.resize();
    else triggerUserInfo.value?.removeResize();
  }
);

const closeUserInfo = () => {
  if (showInfo.value) showInfo.value = false;
};

// 当前查看用户资料卡片的用户userId值
const currentId = ref<string>("");
/**
 * 当前用户点击事件
 */
const currentEv = ref<any>();

// 更新卡片的位置
const updatePosition = () =>
  triggerUserInfo.value?.computedDirection(
    userInfoRef.value?.$refs.userInfoRef as HTMLElement,
    currentEv.value
  );

// 打开用户信息卡片
const openInfo = (userId: string, $event: MouseEvent) => {
  currentId.value = userId;
  if (!showInfo.value) showInfo.value = true;
  currentEv.value = $event;
  nextTick(() => updatePosition());
};

// 重新发送消息
const retryMess = (payload: {
  id: string;
  content: string;
  type: "text" | "voice";
  duration?: number;
}): void => {
  const { id, content, type, duration } = payload;
  if (type === "text") emits("retrySendMessageTxt", id, content);
  else if (payload.type === "voice" && duration)
    emits("retrySendMessageRecord", {
      id,
      content,
      duration,
    });
};

/**
 * 是否显示操作组件
 */
const show = ref<boolean>(false);
const timing = ref();
const updateEle = () => {
  const operMenuEle: HTMLElement = document.getElementsByClassName(
    "colRight-messages__main"
  )[0] as HTMLElement;
  triggerOper.value = new Trigger(operMenuEle);
  const userInfoEle: HTMLElement = document.getElementById(
    "chat-col__messages"
  ) as HTMLElement;
  triggerUserInfo.value = new Trigger(userInfoEle);
};
onMounted(() => {
  socketStore.SOCKET_SUBSRCIBE({
    url: `/user/${Cookie.getCookie("token")}/isInputing`,
    callback: (res: Stomp.Message) => {
      if (timing.value) clearTimeout(timing.value);
      messageStore.isInputing = JSON.parse(res.body).isInputing;
      timing.value = setTimeout(() => {
        if (messageStore.isInputing) messageStore.isInputing = false;
      }, 3000);
    },
    headers: { id: "sub-isInputing" },
  });
  setTimeout(() => {
    updateEle();
    globalThis.addEventListener("resize", updateEle);
    show.value = true;
  });
});

onBeforeUnmount(() => {
  socketStore.SOCKET_UNSUBSRCIBE({
    id: '"sub-isInputing"',
    url: `/user/${Cookie.getCookie("token")}/isInputing`,
  });
  triggerOper.value?.removeResize();
  triggerUserInfo.value?.removeResize();
  if (messageStore.isInputing) messageStore.isInputing = false;
  globalThis.removeEventListener("resize", updateEle);
});

defineExpose({
  closeMenu,
  closeUserInfo,
  scrollBottom,
});
</script>

<style lang="scss" scoped>
.record {
  height: 309px;
  padding: 15px 22px;
  box-sizing: border-box;

  .loadingCircle {
    font-size: 1rem;
    animation: loading-rotate__spinMess 1.5s linear infinite;
    vertical-align: -3px;
    margin-right: 3px;

    &-opearMess {
      animation: loading-rotate__spinMess 1.5s linear infinite;
      margin-top: 8px;

      &__me {
        margin-right: 8px;
        float: left;
      }

      &__other {
        margin-left: 8px;
        float: right;
      }
    }
  }

  &-loadMore {
    text-align: center;
    font-size: 13px;
    color: $dimFontCor;

    span {
      cursor: pointer;
    }
  }

  &-time {
    text-align: center;
    font-size: 13px;
    color: $dimFontCor;

    span {
      padding: 6px 10px;
      border-radius: 3px;
      background-color: #f3f5f8;
    }
  }
}
.chatter {
  &-check {
    @include setWidHei(20px, 20px);
    margin-top: 17px;
    border-radius: 50%;
    transition: background-color 0.25s;

    &__other {
      float: left;
      margin-right: 15px;
    }

    &__me {
      float: right;
      margin-left: 15px;
    }
  }

  &-avatar {
    @include setWidHei(40px, 40px);
    border-radius: 50%;
    margin: 10px 0;
    object-fit: cover;
    cursor: pointer;
  }

  &-other {
    float: left;
    position: relative;
  }

  &-me {
    float: right;
    position: relative;
  }

  &-disabled {
    position: absolute;
    inset: 0;
    z-index: 2;
  }

  &-info {
    margin: 10px 0 6px;
    display: inline-block;

    &__last {
      margin-bottom: 15px;
    }

    &__content {
      word-break: break-all;
      max-width: 248px;
      padding: 8px 10px 10px;
      font-size: 15px;
      letter-spacing: 1px;
      border-radius: 4px;
      display: inline-block;
      text-align: left;
      margin-bottom: 4px;
      box-sizing: border-box;

      & > p {
        margin: 0;
      }

      img {
        @include setWidHei(150px, 150px);
        object-fit: cover;
        cursor: pointer;
      }

      @mixin thame($angle) {
        content: "";
        position: absolute;
        top: 15px;
        border-width: 7px;
        border-style: solid;
        transform: rotate($angle);
      }

      &--other {
        background-color: #f5f7fa;
        position: relative;
        margin-left: 15px;

        &::before {
          left: -6px;
          @include thame(-45deg);
          border-color: transparent transparent transparent #f5f7fa;
        }
      }

      &--me {
        background-color: #79bbff;
        position: relative;
        margin-right: 15px;
        float: right;

        &::after {
          right: -6px;
          border-color: #79bbff transparent transparent transparent;
          @include thame(45deg);
        }
      }
    }

    &__time {
      margin: 0;
      font-size: 13px;
      color: $dimFontCor;
      text-align: center;
      clear: both;
    }
  }
}
</style>
